package mylittleinvestigations.CaseInformation;

import java.io.IOException;
import java.io.InputStream;
import java.util.Stack;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.newdawn.slick.Color;
import org.newdawn.slick.geom.Rectangle;
import org.newdawn.slick.geom.Polygon;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class CaseXmlReader
{
    private Document document;
    private Stack<Element> elementStack;
    private Stack<String> nodeListElementNameStack;
    private NodeList nodeList;
    private Stack<Integer> nodeIndexStack;
    
    public CaseXmlReader(InputStream inputStream) throws SAXException, IOException, ParserConfigurationException
    {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = dbf.newDocumentBuilder();
        this.document = db.parse(inputStream);
        this.document.getDocumentElement().normalize();
        
        this.elementStack = new Stack<Element>();
        this.nodeListElementNameStack = new Stack<String>();
        this.nodeIndexStack = new Stack<Integer>();
    }
    
    public void StartElement(String elementName)
    {
        Element firstDirectChildElement = this.GetFirstDirectChildElementByName(elementName);
        
        if (firstDirectChildElement == null)
        {
            String exceptionMessage = "Cannot start an element that doesn't exist: '" + elementName + "'." + System.lineSeparator() + System.lineSeparator();
            exceptionMessage += "Current XML element name stack:" + System.lineSeparator() + System.lineSeparator();
            
            for (int i = 0; i < this.elementStack.size(); i++)
            {
                for (int j = 0; j < i; j++)
                {
                    exceptionMessage += "  ";
                }
                
                exceptionMessage += "<" + this.elementStack.get(i).getNodeName() + ">" + System.lineSeparator();
            }

            throw new IllegalStateException(exceptionMessage);
        }
        else
        {
            this.elementStack.push(firstDirectChildElement);
        }
    }
    
    public boolean ElementExists(String elementName)
    {
        return this.GetFirstDirectChildElementByName(elementName) != null;
    }
    
    private Element GetFirstDirectChildElementByName(String elementName)
    {
        NodeList nodeList = this.GetNodeListForElementName(elementName);
        
        for (int i = 0; i < nodeList.getLength(); i++)
        {
            if (
                nodeList.item(i).getNodeType() == Node.ELEMENT_NODE &&
                nodeList.item(i).getParentNode() == (this.elementStack.empty() ? document : this.elementStack.peek()))
            {
                return (Element)nodeList.item(i);
            }
        }
        
        return null;
    }
    
    public void EndElement()
    {
        this.elementStack.pop();
    }
    
    private NodeList GetNodeListForElementName(String elementName)
    {
        return
            this.elementStack.empty() ?
                document.getElementsByTagName(elementName) :
                this.elementStack.peek().getElementsByTagName(elementName);
    }
    
    public void StartList(String listElementName)
    {
        this.nodeListElementNameStack.push(listElementName);
        this.nodeList = null;
        this.nodeIndexStack.push(-1);
    }
    
    public boolean MoveToNextListItem()
    {
        int nodeListIndex = this.nodeIndexStack.peek();
        
        if (nodeListIndex >= 0)
        {
            this.elementStack.pop();
        }
        
        if (this.nodeList == null)
        {
            // The way NodeLists work, they become invalid if you've retrieved another one since this one,
            // so we need to get the node list anew each time.
            this.nodeList = this.GetNodeListForElementName(this.nodeListElementNameStack.peek());
        }
        
        nodeListIndex++;
        
        // Ignore all nodes that are not direct children
        // of the current element.
        while (nodeListIndex < this.nodeList.getLength() && this.nodeList.item(nodeListIndex).getParentNode() != this.elementStack.peek())
        {
            nodeListIndex++;
        }
        
        if (nodeListIndex >= nodeList.getLength() || nodeList.item(nodeListIndex).getNodeType() != Node.ELEMENT_NODE)
        {
            this.nodeIndexStack.pop();
            this.nodeListElementNameStack.pop();
            this.nodeList = null;
            return false;
        }
        else
        {
            this.elementStack.push((Element)nodeList.item(nodeListIndex));
            this.nodeIndexStack.pop();
            this.nodeIndexStack.push(nodeListIndex);
        }
        
        return true;
    }
    
    public int ReadIntElement(String elementName)
    {
        return Integer.parseInt(this.ReadTextElement(elementName));
    }

    public double ReadDoubleElement(String elementName)
    {
        return Double.parseDouble(this.ReadTextElement(elementName));
    }

    public boolean ReadBooleanElement(String elementName)
    {
        return Boolean.parseBoolean(this.ReadTextElement(elementName));
    }
    
    public String ReadTextElement(String elementName)
    {
        this.StartElement(elementName);
        String elementValue = this.elementStack.peek().getFirstChild().getNodeValue();
        this.EndElement();
        
        return elementValue;
    }

    public Rectangle ReadRectangleElement()
    {
        this.StartElement("Rectangle");
        Rectangle rectangle =
                new Rectangle(
                        (float)this.ReadDoubleElement("X"),
                        (float)this.ReadDoubleElement("Y"),
                        (float)this.ReadDoubleElement("Width"),
                        (float)this.ReadDoubleElement("Height"));
        this.EndElement();
        
        return rectangle;
    }

    public Color ReadColorElement()
    {
        this.StartElement("Color");
        Color color =
                new Color(
                        (float)this.ReadDoubleElement("R"),
                        (float)this.ReadDoubleElement("G"),
                        (float)this.ReadDoubleElement("B"),
                        (float)this.ReadDoubleElement("A"));
        this.EndElement();
        
        return color;
    }

    public Polygon ReadPolygonElement()
    {
        this.StartElement("Polygon");
        
        Polygon polygon = new Polygon();
        this.StartList("Vertex");
        
        while (this.MoveToNextListItem())
        {
            double x = this.ReadDoubleElement("X");
            double y = this.ReadDoubleElement("Y");
            
            polygon.addPoint((float)x, (float)y);
        }
        
        this.EndElement();
        
        return polygon;
    }
}
